// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

/// example:
/// bazel build //cc/appcoin2/support/feed:plot
/// ./bazel-bin/cc/appcoin2/support/feed/plot --driver ./data/coin2/feed/examples/...json

#include <boost/algorithm/string.hpp>
#include <cxxopts.hpp>

#include "coin2/app/driver.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/config/args.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/feed.pb.h"
#include "coin2/strategy/strategy.h"
#include "presto/quant/math/moving_average.h"
#include "vitro.h"  // NOLINT

using coin2::exchange::base::feed::FeedUpdate;
using namespace coin2::app;
using namespace coin2::base::config;
using presto::math::TimeWindowMovingAverage;

struct ProductData {
  std::vector<int64_t> book_timestamps;
  std::vector<int64_t> trade_timestamps;

  std::vector<int64_t> book_timestamps_et;
  std::vector<int64_t> trade_timestamps_et;

  std::vector<double> book_timestamps_ftet;
  std::vector<double> trade_timestamps_ftet;

  std::vector<double> midp;
  // std::vector<double> spread;
  std::vector<double> spread_5s;
  std::vector<double> trade_qty;
  std::vector<double> trade_prices;
  std::vector<double> ask_1bps_qty;
  std::vector<double> bid_1bps_qty;

  TimeWindowMovingAverage<double> spread_5s_window;

  // interval
  std::vector<double> trade_intervals_ms;
  std::vector<double> book_intervals_ms;
  int64_t last_trade_ts;
  int64_t last_book_ts;
};

int64_t max_ftet_book = 0L;
int64_t max_ftet_trade = 0L;
std::map<std::string, ProductData> data;

struct Strategy : public ::coin2::strategy::IStrategy {
  void onTradeFeed(const FeedUpdate& upd) override {
    auto ts = upd.timestamp();
    auto id = upd.system_name() + ":" + upd.product().absolute_norm();
    auto& item = data[id];

    auto& trade = upd.trade();
    item.trade_timestamps.push_back(ts);
    item.trade_timestamps_et.push_back(upd.trade().timestamp);
    item.trade_timestamps_ftet.push_back(ts - upd.trade().timestamp);
    max_ftet_trade = std::max(max_ftet_trade, ts - upd.trade().timestamp);

    item.trade_prices.push_back(trade.price);
    item.trade_qty.push_back(trade.fill_qty);
    if (item.last_trade_ts > 0) {
      auto interval = static_cast<double>(ts - item.last_trade_ts) * 1e-6;
      if (interval > 0) {
        item.trade_intervals_ms.push_back(interval);
      }
    }
    item.last_trade_ts = ts;
  }
  void onBookFeed(const FeedUpdate& upd) override {
    auto ts = upd.timestamp();
    auto& book = upd.book();
    if (book.Ask0() && book.Bid0()) {
      auto id = upd.system_name() + ":" + upd.product().absolute_norm();
      auto& item = data[id];

      item.book_timestamps.push_back(ts);
      item.book_timestamps_et.push_back(upd.book().Timestamp());
      item.book_timestamps_ftet.push_back(ts - upd.book().Timestamp());
      max_ftet_book = std::max(max_ftet_book, ts - upd.book().Timestamp());

      auto ask = book.Ask0()->price;
      auto bid = book.Bid0()->price;
      auto mid = (ask + bid) * 0.5;
      item.midp.push_back(mid);

      {
      double spread = (ask - bid) / mid * 10000.0;
        // item.spread.push_back(spread);

        if (item.spread_5s.empty()) {
          item.spread_5s_window.ResizeWindow(5'000'000'000);
        }
        item.spread_5s_window.Update(ts, spread);
        item.spread_5s.push_back(item.spread_5s_window.average());
      }

      /*
      // midp +- 1bps
      {
        double bound = book.Ask0()->price * 1.0001;
        double sum = 0;
        for (auto& [p, q] : book.Asks()) {
          if (p > bound) {
            break;
          }
          sum += q;
        }
        item.ask_1bps_qty.push_back(sum);
      }
      {
        double bound = book.Bid0()->price * 0.9999;
        double sum = 0;
        for (auto& [p, q] : book.Bids()) {
          if (p < bound) {
            break;
          }
          sum += q;
        }
        item.bid_1bps_qty.push_back(-sum);
      }*/

      if (item.last_book_ts > 0) {
        auto interval = static_cast<double>(ts - item.last_book_ts) * 1e-6;
        if (interval > 0) {
          item.book_intervals_ms.push_back(interval);
        }
      }
      item.last_book_ts = ts;
    }
  }
};

using coin2::app::DriverConfig;

DriverConfig parse_cli(int argc, char* argv[], std::string* out_filename) {
  // you have 2 options to provide flags.
  // 1. --driver
  // 2. --machine --fastfeed --...
  // mixing these 2 is discouraged.

  DriverConfig app;

  cxxopts::Options opt("Feed Plot", "Plot feed");
  AddDriverOptions(&opt);                                         // --driver
  AddIntervalOptions(&opt);                                       // --start --end --duration
  opt.add_options()                                               // add options
      ("fastfeed", "use fastfeed")                                //
      ("raw", "use raw")                                          //
      ("out", "out", cxxopts::value<std::string>()->default_value("feedplot.png"))
      ("machine", "feed machine", cxxopts::value<std::string>())  //
      ("mea", "M.E.A", cxxopts::value<std::string>())             //
      ("recipe", "recipe", cxxopts::value<std::string>())         //
      ("product", "a norm symbol", cxxopts::value<std::vector<std::string>>());  //

  auto res = opt.parse(argc, argv);

  if (!ParseDriverOptions(res, &app)) {
    auto& exchanges = *app.mutable_feed()->mutable_exchanges();
    auto& exchange = exchanges[res["mea"].as<std::string>()];

    exchange.set_mea(res["mea"].as<std::string>());
    exchange.set_recipe(res["recipe"].as<std::string>());
    exchange.set_type(coin2::exchange::feed::FeedSystemType::FEED_SYSTEM_TYPE_ARCHIVE);
    for (const auto& norm : res["product"].as<std::vector<std::string>>()) {
      exchange.mutable_products()->add_norms(norm);
    }

    if (!(res["fastfeed"].as<bool>() ^ res["raw"].as<bool>())) {
      THROW() << "You should provide only one of --fastfeed or --raw";
    }
    bool use_fastfeed = res["fastfeed"].as<bool>();
    exchange.mutable_archive()->set_archive_type(
        use_fastfeed ? coin2::exchange::feed::ArchiveConfig::FASTFEED
                     : coin2::exchange::feed::ArchiveConfig::RAW);
    exchange.mutable_archive()->set_machine(res["machine"].as<std::string>());
  }
  ParseIntervalOptions(
      res,
      app.mutable_feed()->mutable_common()->mutable_archive()->mutable_interval());

  *out_filename = res["out"].as<std::string>();

  return app;
}

int main(int argc, char* argv[]) {
  /* ----------------------------------- opt ---------------------------------- */
  std::string out_filename;
  coin2::app::DriverConfig app = parse_cli(argc, argv, &out_filename);

  /* ----------------------------------- run ---------------------------------- */
  printf("reading ..\n");

  Strategy strategy;
  ::coin2::app::Driver driver(app, &strategy);
  driver.Run();

  std::experimental::filesystem::path fpath(out_filename);

  bool has_ftet_book = true;
  bool has_ftet_trade = true;
  if (max_ftet_book > 36000e9) {
    max_ftet_book = 0;
    has_ftet_book = false;
  }
  if (max_ftet_trade > 36000e9) {
    max_ftet_trade = 0;
    has_ftet_trade = false;
  }

  if (data.size() == 0) {
    printf("Empty!");
    return 0;
  }

  std::string max_ftet_str = fmt::format("{0:06.2f}s_", (max_ftet_book * 1e-9));
  fpath.replace_filename(
      max_ftet_str + std::string(fpath.stem().c_str()) + fpath.extension().c_str());

  /* ---------------------------------- dump ---------------------------------- */
  printf("Plotting %lu products to %s ..", data.size(), fpath.c_str());

  // auto interval = GetEnclosingInterval(app);

  Figure fig(5, 1);
  fig.title = fpath.stem();
  fig.width = 1600;
  fig.height = 1600;
  fig.height_ratios = {5, 1, 1, 1, 1};

  /* ---------------------------- plot 1 - askp, midp, bidp, trade ----------- */
  {
    auto& ax = fig.axes(1, 1);
    ax.is_x_nanotimestamps = true;
    ax.grid = true;
    ax.legend = "lower right";
    std::string text;
    for (auto& [key, item] : data) {
      {
        auto& line = ax.line(
            key + ":FT_book_#" + std::to_string(item.book_timestamps.size()),
            item.book_timestamps, item.midp);
        line.alpha = 0.8;
        line.width = 0.2;
        line.color = "y";
        line.drawstyle = "steps-post";
      }
      {
        auto& scatter = ax.scatter(
            key + ":FT_trd_#" + std::to_string(item.trade_timestamps.size()),
            item.trade_timestamps, item.trade_prices);
        scatter.marker_size = 0.05;
        scatter.width = 0.05;
        scatter.alpha = 0.8;
        scatter.color = "y";
      }
      if (has_ftet_book) {
        auto& line = ax.line(
            key + ":ET_book_#" + std::to_string(item.book_timestamps_et.size()),
            item.book_timestamps_et, item.midp);
        line.alpha = 0.8;
        line.width = 0.2;
        line.color = "b";
        line.drawstyle = "steps-post";
      }
      if (has_ftet_trade) {
        auto& scatter = ax.scatter(
            key + ":ET_trd_#" + std::to_string(item.trade_timestamps_et.size()),
            item.trade_timestamps_et, item.trade_prices);
        scatter.marker_size = 0.05;
        scatter.width = 0.05;
        scatter.alpha = 0.8;
        scatter.color = "b";
      }
    }
    ax.text(text, 0.95, 0.05);
  }

  /* ---------------------------- plot 2 - ft - et (book) ----------- */
  if (has_ftet_book) {
    auto& ax = fig.axes(2, 1);
    ax.title = "Book Ft - Et (ns)";
    ax.is_x_nanotimestamps = true;
    for (auto& [key, item] : data) {
      {
        auto& line = ax.line(key + ":ft-et", item.book_timestamps, item.book_timestamps_ftet);
        line.width = 1;
        line.color = "k";
      }
    }
  }

  /* ---------------------------- plot 3 - ft - et (trade) ----------- */
  if (has_ftet_trade) {
    auto& ax = fig.axes(3, 1);
    ax.title = "Trade Ft - Et (ns)";
    ax.is_x_nanotimestamps = true;
    for (auto& [key, item] : data) {
      {
        auto& line = ax.line(key + ":ft-et", item.trade_timestamps, item.trade_timestamps_ftet);
        line.width = 1;
        line.color = "k";
      }
    }
  }

  /* ---------------------------- plot 4 - spread ----- */
  {
    auto& ax = fig.axes(4, 1);
    // auto& twin = fig.twinx(2, 1);
    ax.title = "Spread (bps)";
    ax.is_x_nanotimestamps = true;
    ax.grid = true;
    // twin.is_x_nanotimestamps = true;
    for (auto& [key, item] : data) {
      {
        auto& line = ax.line(key + ":spread-5s", item.book_timestamps, item.spread_5s);
        line.alpha = 1.0;
        line.width = 0.4;
        line.drawstyle = "steps-post";
      }
    }
  }

  /* ---------------------------- plot 5 - volume (book) ----------- */
  /*
  {
    auto& ax = fig.axes(4, 1);
    ax.title = "Volume (Book Qty within 1bps)";
    ax.xlabel = "bid (-) <- qty -> ask (+)";
    ax.ylabel = "CDF";
    ax.legend = "upper left";
    ax.xlim_left = -500;
    ax.xlim_right = +500;
    ax.xtick_major = "multiple";
    ax.xtick_major_multiple = 50;
    ax.grid = true;

    for (auto& [key, item] : data) {
      auto& ask = ax.histogram({key + ":ask-1bps"}, 1000, {item.ask_1bps_qty});
      ask.cumulative = -1;
      ask.alpha = 1.0;
      ask.normalize_area = true;

      auto& bid = ax.histogram({key + ":bid-1bps"}, 1000, {item.bid_1bps_qty});
      bid.cumulative = 1;
      bid.alpha = 1.0;
      bid.normalize_area = true;
    }
  }*/

  /* ---------------------------- plot 6 - interval ----------- */
  {
    auto& ax = fig.axes(5, 1);
    ax.title = "Feed interval";
    ax.xscale = "log";
    ax.xlabel = "interval (ms)";
    ax.ylabel = "book";
    ax.legend = "upper left";

    for (auto& [key, item] : data) {
      {
        if (item.book_intervals_ms.size() == 0)
        {
          continue;
        }
        auto& h = ax.histogram({key + ":book"}, 1000, {item.book_intervals_ms});
        h.bin_log_scale = true;
      }
    }
  }

  {
    auto& ax = fig.twinx(5, 1);
    ax.ylabel = "trade";
    ax.legend = "upper right";

    for (auto& [key, item] : data) {
      {
        if (item.trade_intervals_ms.size() == 0)
        {
          continue;
        }
        auto& h = ax.histogram({key + ":trade"}, 1000, {item.trade_intervals_ms});
        h.bin_log_scale = true;
      }
    }
  }

  Matplot matplot(fig);
  matplot.save(fpath);

  return 0;
}
